/*
===========================================================================
Copyright (C) 1999-2005 Id Software, Inc.
 
This file is part of Quake III Arena source code.
 
Quake III Arena source code is free software; you can redistribute it
and/or modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of the License,
or (at your option) any later version.
 
Quake III Arena source code is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
 
You should have received a copy of the GNU General Public License
along with Foobar; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
===========================================================================
*/

/*****************************************************************************
 * name:		snd_dma.c
 *
 * desc:		main control for any streaming sound output device
 *
 * $Archive: /code/client/snd_dma.c $
 *
 *****************************************************************************/

#include "snd_local.h"
#include "client.h"

void S_Play_f(void);
void S_SoundList_f(void);
void S_Music_f(void);

void S_Update_(void);
void S_StopAllSounds(void);
void S_UpdateBackgroundTrack( void );

static fileHandle_t s_backgroundFile;
static wavinfo_t	s_backgroundInfo;
//int			s_nextWavChunk;
static int			s_backgroundSamples;
static char		s_backgroundLoop[MAX_QPATH];
//static char		s_backgroundMusic[MAX_QPATH]; //TTimo: unused


// =======================================================================
// Internal sound data & structures
// =======================================================================

// only begin attenuating sound volumes when outside the FULLVOLUME range
#define		SOUND_FULLVOLUME	80

#define		SOUND_ATTENUATE		0.0008f

channel_t   s_channels[MAX_CHANNELS];
channel_t   loop_channels[MAX_CHANNELS];
int			numLoopChannels;

static int	s_soundStarted;
static		qboolean	s_soundMuted;

dma_t		dma;

static int			listener_number;
static vec3_t		listener_origin;
static vec3_t		listener_axis[3];

int			s_soundtime;		// sample PAIRS
int   		s_paintedtime; 		// sample PAIRS

// MAX_SFX may be larger than MAX_SOUNDS because
// of custom player sounds
#define		MAX_SFX			4096
sfx_t		s_knownSfx[MAX_SFX];
int			s_numSfx = 0;

#define		LOOP_HASH		128
static	sfx_t		*sfxHash[LOOP_HASH];

cvar_t		*s_volume;
cvar_t		*s_testsound;
cvar_t		*s_khz;
cvar_t		*s_show;
cvar_t		*s_mixahead;
cvar_t		*s_mixPreStep;
cvar_t		*s_musicVolume;
cvar_t		*s_separation;
cvar_t		*s_doppler;

static loopSound_t		loopSounds[MAX_GENTITIES];
static	channel_t		*freelist = NULL;

int						s_rawend;
portable_samplepair_t	s_rawsamples[MAX_RAW_SAMPLES];


// ====================================================================
// User-setable variables
// ====================================================================


void S_SoundInfo_f(void)
{
    Com_Printf("----- Sound Info -----\n" );
    if (!s_soundStarted)
    {
        Com_Printf ("sound system not started\n");
    }
    else
    {
        if ( s_soundMuted )
        {
            Com_Printf ("sound system is muted\n");
        }

        Com_Printf("%5d stereo\n", dma.channels - 1);
        Com_Printf("%5d samples\n", dma.samples);
        Com_Printf("%5d samplebits\n", dma.samplebits);
        Com_Printf("%5d submission_chunk\n", dma.submission_chunk);
        Com_Printf("%5d speed\n", dma.speed);
        Com_Printf("0x%x dma buffer\n", dma.buffer);
        if ( s_backgroundFile )
        {
            Com_Printf("Background file: %s\n", s_backgroundLoop );
        }
        else
        {
            Com_Printf("No background file.\n" );
        }

    }
    Com_Printf("----------------------\n" );
}



/*
================
S_Init
================
*/
void S_Init( void )
{
    cvar_t	*cv;
    qboolean	r;

    Com_Printf("\n------- sound initialization -------\n");

    s_volume = Cvar_Get ("s_volume", "0.8", CVAR_ARCHIVE);
    s_musicVolume = Cvar_Get ("s_musicvolume", "0.25", CVAR_ARCHIVE);
    s_separation = Cvar_Get ("s_separation", "0.5", CVAR_ARCHIVE);
    s_doppler = Cvar_Get ("s_doppler", "1", CVAR_ARCHIVE);
    s_khz = Cvar_Get ("s_khz", "22", CVAR_ARCHIVE);
    s_mixahead = Cvar_Get ("s_mixahead", "0.2", CVAR_ARCHIVE);

    s_mixPreStep = Cvar_Get ("s_mixPreStep", "0.05", CVAR_ARCHIVE);
    s_show = Cvar_Get ("s_show", "0", CVAR_CHEAT);
    s_testsound = Cvar_Get ("s_testsound", "0", CVAR_CHEAT);

    cv = Cvar_Get ("s_initsound", "1", 0);
    if ( !cv->integer )
    {
        Com_Printf ("not initializing.\n");
        Com_Printf("------------------------------------\n");
        return;
    }

    Cmd_AddCommand("play", S_Play_f);
    Cmd_AddCommand("music", S_Music_f);
    Cmd_AddCommand("s_list", S_SoundList_f);
    Cmd_AddCommand("s_info", S_SoundInfo_f);
    Cmd_AddCommand("s_stop", S_StopAllSounds);

    r = SNDDMA_Init();
    Com_Printf("------------------------------------\n");

    if ( r )
    {
        s_soundStarted = 1;
        s_soundMuted = 1;
//		s_numSfx = 0;

        Com_Memset(sfxHash, 0, sizeof(sfx_t *)*LOOP_HASH);

        s_soundtime = 0;
        s_paintedtime = 0;

        S_StopAllSounds ();

        S_SoundInfo_f();
    }

}


void S_ChannelFree(channel_t *v)
{
    v->thesfx = NULL;
    *(channel_t **)v = freelist;
    freelist = (channel_t*)v;
}

channel_t*	S_ChannelMalloc(void)
{
    channel_t *v;
    if (freelist == NULL)
    {
        return NULL;
    }
    v = freelist;
    freelist = *(channel_t **)freelist;
    v->allocTime = Com_Milliseconds();
    return v;
}

void S_ChannelSetup(void)
{
    channel_t *p, *q;

    // clear all the sounds so they don't
    Com_Memset( s_channels, 0, sizeof( s_channels ) );

    p = s_channels;
    ;
    q = p + MAX_CHANNELS;
    while (--q > p)
    {
        *(channel_t **)q = q-1;
    }

    *(channel_t **)q = NULL;
    freelist = p + MAX_CHANNELS - 1;
    Com_DPrintf("Channel memory manager started\n");
}

// =======================================================================
// Shutdown sound engine
// =======================================================================

void S_Shutdown( void )
{
    if ( !s_soundStarted )
    {
        return;
    }

    SNDDMA_Shutdown();

    s_soundStarted = 0;

    Cmd_RemoveCommand("play");
    Cmd_RemoveCommand("music");
    Cmd_RemoveCommand("stopsound");
    Cmd_RemoveCommand("soundlist");
    Cmd_RemoveCommand("soundinfo");
}


// =======================================================================
// Load a sound
// =======================================================================

/*
================
return a hash value for the sfx name
================
*/
static long S_HashSFXName(const char *name)
{
    int		i;
    long	hash;
    char	letter;

    hash = 0;
    i = 0;
    while (name[i] != '\0')
    {
        letter = tolower(name[i]);
        if (letter =='.') break;				// don't include extension
        if (letter =='\\') letter = '/';		// damn path names
        hash+=(long)(letter)*(i+119);
        i++;
    }
    hash &= (LOOP_HASH-1);
    return hash;
}

/*
==================
S_FindName
 
Will allocate a new sfx if it isn't found
==================
*/
static sfx_t *S_FindName( const char *name )
{
    int		i;
    int		hash;

    sfx_t	*sfx;

    if (!name)
    {
        Com_Error (ERR_FATAL, "S_FindName: NULL\n");
    }
    if (!name[0])
    {
        Com_Error (ERR_FATAL, "S_FindName: empty name\n");
    }

    if (strlen(name) >= MAX_QPATH)
    {
        Com_Error (ERR_FATAL, "Sound name too long: %s", name);
    }

    hash = S_HashSFXName(name);

    sfx = sfxHash[hash];
    // see if already loaded
    while (sfx)
    {
        if (!Q_stricmp(sfx->soundName, name) )
        {
            return sfx;
        }
        sfx = sfx->next;
    }

    // find a free sfx
    for (i=0 ; i < s_numSfx ; i++)
    {
        if (!s_knownSfx[i].soundName[0])
        {
            break;
        }
    }

    if (i == s_numSfx)
    {
        if (s_numSfx == MAX_SFX)
        {
            Com_Error (ERR_FATAL, "S_FindName: out of sfx_t");
        }
        s_numSfx++;
    }

    sfx = &s_knownSfx[i];
    Com_Memset (sfx, 0, sizeof(*sfx));
    strcpy (sfx->soundName, name);

    sfx->next = sfxHash[hash];
    sfxHash[hash] = sfx;

    return sfx;
}

/*
=================
S_DefaultSound
=================
*/
void S_DefaultSound( sfx_t *sfx )
{

    int		i;

    sfx->soundLength = 512;
    sfx->soundData = SND_malloc();
    sfx->soundData->next = NULL;


    for ( i = 0 ; i < sfx->soundLength ; i++ )
    {
        sfx->soundData->sndChunk[i] = i;
    }
}

/*
===================
S_DisableSounds
 
Disables sounds until the next S_BeginRegistration.
This is called when the hunk is cleared and the sounds
are no longer valid.
===================
*/
void S_DisableSounds( void )
{
    S_StopAllSounds();
    s_soundMuted = qtrue;
}

/*
=====================
S_BeginRegistration
 
=====================
*/
void S_BeginRegistration( void )
{
    s_soundMuted = qfalse;		// we can play again

    if (s_numSfx == 0)
    {
        SND_setup();

        s_numSfx = 0;
        Com_Memset( s_knownSfx, 0, sizeof( s_knownSfx ) );
        Com_Memset(sfxHash, 0, sizeof(sfx_t *)*LOOP_HASH);

        S_RegisterSound("sound/feedback/hit.wav", qfalse);		// changed to a sound in baseq3
    }
}


/*
==================
S_RegisterSound
 
Creates a default buzz sound if the file can't be loaded
==================
*/
sfxHandle_t	S_RegisterSound( const char *name, qboolean compressed )
{
    sfx_t	*sfx;

    compressed = qfalse;
    if (!s_soundStarted)
    {
        return 0;
    }

    if ( strlen( name ) >= MAX_QPATH )
    {
        Com_Printf( "Sound name exceeds MAX_QPATH\n" );
        return 0;
    }

    sfx = S_FindName( name );
    if ( sfx->soundData )
    {
        if ( sfx->defaultSound )
        {
            Com_Printf( S_COLOR_YELLOW "WARNING: could not find %s - using default\n", sfx->soundName );
            return 0;
        }
        return sfx - s_knownSfx;
    }

    sfx->inMemory = qfalse;
    sfx->soundCompressed = compressed;

    S_memoryLoad(sfx);

    if ( sfx->defaultSound )
    {
        Com_Printf( S_COLOR_YELLOW "WARNING: could not find %s - using default\n", sfx->soundName );
        return 0;
    }

    return sfx - s_knownSfx;
}

void S_memoryLoad(sfx_t	*sfx)
{
    // load the sound file
    if ( !S_LoadSound ( sfx ) )
    {
//		Com_Printf( S_COLOR_YELLOW "WARNING: couldn't load sound: %s\n", sfx->soundName );
        sfx->defaultSound = qtrue;
    }
    sfx->inMemory = qtrue;
}

//=============================================================================

/*
=================
S_SpatializeOrigin
 
Used for spatializing s_channels
=================
*/
void S_SpatializeOrigin (vec3_t origin, int master_vol, int *left_vol, int *right_vol)
{
    vec_t		dot;
    vec_t		dist;
    vec_t		lscale, rscale, scale;
    vec3_t		source_vec;
    vec3_t		vec;

    const float dist_mult = SOUND_ATTENUATE;

    // calculate stereo seperation and distance attenuation
    VectorSubtract(origin, listener_origin, source_vec);

    dist = VectorNormalize(source_vec);
    dist -= SOUND_FULLVOLUME;
    if (dist < 0)
        dist = 0;			// close enough to be at full volume
    dist *= dist_mult;		// different attenuation levels

    VectorRotate( source_vec, listener_axis, vec );

    dot = -vec[1];

    if (dma.channels == 1)
    { // no attenuation = no spatialization
        rscale = 1.0;
        lscale = 1.0;
    }
    else
    {
        rscale = 0.5 * (1.0 + dot);
        lscale = 0.5 * (1.0 - dot);
        //rscale = s_separation->value + ( 1.0 - s_separation->value ) * dot;
        //lscale = s_separation->value - ( 1.0 - s_separation->value ) * dot;
        if ( rscale < 0 )
        {
            rscale = 0;
        }
        if ( lscale < 0 )
        {
            lscale = 0;
        }
    }

    // add in distance effect
    scale = (1.0 - dist) * rscale;
    *right_vol = (master_vol * scale);
    if (*right_vol < 0)
        *right_vol = 0;

    scale = (1.0 - dist) * lscale;
    *left_vol = (master_vol * scale);
    if (*left_vol < 0)
        *left_vol = 0;
}

// =======================================================================
// Start a sound effect
// =======================================================================

/*
====================
S_StartSound
 
Validates the parms and ques the sound up
if pos is NULL, the sound will be dynamically sourced from the entity
Entchannel 0 will never override a playing sound
====================
*/
void S_StartSound(vec3_t origin, int entityNum, int entchannel, sfxHandle_t sfxHandle )
{
    channel_t	*ch;
    sfx_t		*sfx;
    int i, oldest, chosen, time;
    int	inplay, allowed;

    if ( !s_soundStarted || s_soundMuted )
    {
        return;
    }

    if ( !origin && ( entityNum < 0 || entityNum > MAX_GENTITIES ) )
    {
        Com_Error( ERR_DROP, "S_StartSound: bad entitynum %i", entityNum );
    }

    if ( sfxHandle < 0 || sfxHandle >= s_numSfx )
    {
        Com_Printf( S_COLOR_YELLOW, "S_StartSound: handle %i out of range\n", sfxHandle );
        return;
    }

    sfx = &s_knownSfx[ sfxHandle ];

    if (sfx->inMemory == qfalse)
    {
        S_memoryLoad(sfx);
    }

    if ( s_show->integer == 1 )
    {
        Com_Printf( "%i : %s\n", s_paintedtime, sfx->soundName );
    }

    time = Com_Milliseconds();

//	Com_Printf("playing %s\n", sfx->soundName);
    // pick a channel to play on

    allowed = 4;
    if (entityNum == listener_number)
    {
        allowed = 8;
    }

    ch = s_channels;
    inplay = 0;
    for ( i = 0; i < MAX_CHANNELS ; i++, ch++ )
    {
        if (ch[i].entnum == entityNum && ch[i].thesfx == sfx)
        {
            if (time - ch[i].allocTime < 50)
            {
//				if (Cvar_VariableValue( "cg_showmiss" )) {
//					Com_Printf("double sound start\n");
//				}
                return;
            }
            inplay++;
        }
    }

    if (inplay>allowed)
    {
        return;
    }

    sfx->lastTimeUsed = time;

    ch = S_ChannelMalloc();	// entityNum, entchannel);
    if (!ch)
    {
        ch = s_channels;

        oldest = sfx->lastTimeUsed;
        chosen = -1;
        for ( i = 0 ; i < MAX_CHANNELS ; i++, ch++ )
        {
            if (ch->entnum != listener_number && ch->entnum == entityNum && ch->allocTime<oldest && ch->entchannel != CHAN_ANNOUNCER)
            {
                oldest = ch->allocTime;
                chosen = i;
            }
        }
        if (chosen == -1)
        {
            ch = s_channels;
            for ( i = 0 ; i < MAX_CHANNELS ; i++, ch++ )
            {
                if (ch->entnum != listener_number && ch->allocTime<oldest && ch->entchannel != CHAN_ANNOUNCER)
                {
                    oldest = ch->allocTime;
                    chosen = i;
                }
            }
            if (chosen == -1)
            {
                if (ch->entnum == listener_number)
                {
                    for ( i = 0 ; i < MAX_CHANNELS ; i++, ch++ )
                    {
                        if (ch->allocTime<oldest)
                        {
                            oldest = ch->allocTime;
                            chosen = i;
                        }
                    }
                }
                if (chosen == -1)
                {
                    Com_Printf("dropping sound\n");
                    return;
                }
            }
        }
        ch = &s_channels[chosen];
        ch->allocTime = sfx->lastTimeUsed;
    }

    if (origin)
    {
        VectorCopy (origin, ch->origin);
        ch->fixed_origin = qtrue;
    }
    else
    {
        ch->fixed_origin = qfalse;
    }

    ch->master_vol = 127;
    ch->entnum = entityNum;
    ch->thesfx = sfx;
    ch->startSample = START_SAMPLE_IMMEDIATE;
    ch->entchannel = entchannel;
    ch->leftvol = ch->master_vol;		// these will get calced at next spatialize
    ch->rightvol = ch->master_vol;		// unless the game isn't running
    ch->doppler = qfalse;
}


/*
==================
S_StartLocalSound
==================
*/
void S_StartLocalSound( sfxHandle_t sfxHandle, int channelNum )
{
    if ( !s_soundStarted || s_soundMuted )
    {
        return;
    }

    if ( sfxHandle < 0 || sfxHandle >= s_numSfx )
    {
        Com_Printf( S_COLOR_YELLOW, "S_StartLocalSound: handle %i out of range\n", sfxHandle );
        return;
    }

    S_StartSound (NULL, listener_number, channelNum, sfxHandle );
}


/*
==================
S_ClearSoundBuffer
 
If we are about to perform file access, clear the buffer
so sound doesn't stutter.
==================
*/
void S_ClearSoundBuffer( void )
{
    int		clear;

    if (!s_soundStarted)
        return;

    // stop looping sounds
    Com_Memset(loopSounds, 0, MAX_GENTITIES*sizeof(loopSound_t));
    Com_Memset(loop_channels, 0, MAX_CHANNELS*sizeof(channel_t));
    numLoopChannels = 0;

    S_ChannelSetup();

    s_rawend = 0;

    if (dma.samplebits == 8)
        clear = 0x80;
    else
        clear = 0;

    SNDDMA_BeginPainting ();
    if (dma.buffer)
        // TTimo: due to a particular bug workaround in linux sound code,
        //   have to optionally use a custom C implementation of Com_Memset
        //   not affecting win32, we have #define Snd_Memset Com_Memset
        // https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=371
        Snd_Memset(dma.buffer, clear, dma.samples * dma.samplebits/8);
    SNDDMA_Submit ();
}

/*
==================
S_StopAllSounds
==================
*/
void S_StopAllSounds(void)
{
    if ( !s_soundStarted )
    {
        return;
    }

    // stop the background music
    S_StopBackgroundTrack();

    S_ClearSoundBuffer ();
}

/*
==============================================================
 
continuous looping sounds are added each frame
 
==============================================================
*/

void S_StopLoopingSound(int entityNum)
{
    loopSounds[entityNum].active = qfalse;
//	loopSounds[entityNum].sfx = 0;
    loopSounds[entityNum].kill = qfalse;
}

/*
==================
S_ClearLoopingSounds
 
==================
*/
void S_ClearLoopingSounds( qboolean killall )
{
    int i;
    for ( i = 0 ; i < MAX_GENTITIES ; i++)
    {
        if (killall || loopSounds[i].kill == qtrue || (loopSounds[i].sfx && loopSounds[i].sfx->soundLength == 0))
        {
            loopSounds[i].kill = qfalse;
            S_StopLoopingSound(i);
        }
    }
    numLoopChannels = 0;
}

/*
==================
S_AddLoopingSound
 
Called during entity generation for a frame
Include velocity in case I get around to doing doppler...
==================
*/
void S_AddLoopingSound( int entityNum, const vec3_t origin, const vec3_t velocity, sfxHandle_t sfxHandle )
{
    sfx_t *sfx;

    if ( !s_soundStarted || s_soundMuted )
    {
        return;
    }

    if ( sfxHandle < 0 || sfxHandle >= s_numSfx )
    {
        Com_Printf( S_COLOR_YELLOW, "S_AddLoopingSound: handle %i out of range\n", sfxHandle );
        return;
    }

    sfx = &s_knownSfx[ sfxHandle ];

    if (sfx->inMemory == qfalse)
    {
        S_memoryLoad(sfx);
    }

    if ( !sfx->soundLength )
    {
        Com_Error( ERR_DROP, "%s has length 0", sfx->soundName );
    }

    VectorCopy( origin, loopSounds[entityNum].origin );
    VectorCopy( velocity, loopSounds[entityNum].velocity );
    loopSounds[entityNum].active = qtrue;
    loopSounds[entityNum].kill = qtrue;
    loopSounds[entityNum].doppler = qfalse;
    loopSounds[entityNum].oldDopplerScale = 1.0;
    loopSounds[entityNum].dopplerScale = 1.0;
    loopSounds[entityNum].sfx = sfx;

    if (s_doppler->integer && VectorLengthSquared(velocity)>0.0)
    {
        vec3_t	out;
        float	lena, lenb;

        loopSounds[entityNum].doppler = qtrue;
        lena = DistanceSquared(loopSounds[listener_number].origin, loopSounds[entityNum].origin);
        VectorAdd(loopSounds[entityNum].origin, loopSounds[entityNum].velocity, out);
        lenb = DistanceSquared(loopSounds[listener_number].origin, out);
        if ((loopSounds[entityNum].framenum+1) != cls.framecount)
        {
            loopSounds[entityNum].oldDopplerScale = 1.0;
        }
        else
        {
            loopSounds[entityNum].oldDopplerScale = loopSounds[entityNum].dopplerScale;
        }
        loopSounds[entityNum].dopplerScale = lenb/(lena*100);
        if (loopSounds[entityNum].dopplerScale<=1.0)
        {
            loopSounds[entityNum].doppler = qfalse;			// don't bother doing the math
        }
    }

    loopSounds[entityNum].framenum = cls.framecount;
}

/*
==================
S_AddLoopingSound
 
Called during entity generation for a frame
Include velocity in case I get around to doing doppler...
==================
*/
void S_AddRealLoopingSound( int entityNum, const vec3_t origin, const vec3_t velocity, sfxHandle_t sfxHandle )
{
    sfx_t *sfx;

    if ( !s_soundStarted || s_soundMuted )
    {
        return;
    }

    if ( sfxHandle < 0 || sfxHandle >= s_numSfx )
    {
        Com_Printf( S_COLOR_YELLOW, "S_AddRealLoopingSound: handle %i out of range\n", sfxHandle );
        return;
    }

    sfx = &s_knownSfx[ sfxHandle ];

    if (sfx->inMemory == qfalse)
    {
        S_memoryLoad(sfx);
    }

    if ( !sfx->soundLength )
    {
        Com_Error( ERR_DROP, "%s has length 0", sfx->soundName );
    }
    VectorCopy( origin, loopSounds[entityNum].origin );
    VectorCopy( velocity, loopSounds[entityNum].velocity );
    loopSounds[entityNum].sfx = sfx;
    loopSounds[entityNum].active = qtrue;
    loopSounds[entityNum].kill = qfalse;
    loopSounds[entityNum].doppler = qfalse;
}



/*
==================
S_AddLoopSounds
 
Spatialize all of the looping sounds.
All sounds are on the same cycle, so any duplicates can just
sum up the channel multipliers.
==================
*/
void S_AddLoopSounds (void)
{
    int			i, j, time;
    int			left_total, right_total, left, right;
    channel_t	*ch;
    loopSound_t	*loop, *loop2;
    static int	loopFrame;


    numLoopChannels = 0;

    time = Com_Milliseconds();

    loopFrame++;
    for ( i = 0 ; i < MAX_GENTITIES ; i++)
    {
        loop = &loopSounds[i];
        if ( !loop->active || loop->mergeFrame == loopFrame )
        {
            continue;	// already merged into an earlier sound
        }

        if (loop->kill)
        {
            S_SpatializeOrigin( loop->origin, 127, &left_total, &right_total);			// 3d
        }
        else
        {
            S_SpatializeOrigin( loop->origin, 90,  &left_total, &right_total);			// sphere
        }

        loop->sfx->lastTimeUsed = time;

        for (j=(i+1); j< MAX_GENTITIES ; j++)
        {
            loop2 = &loopSounds[j];
            if ( !loop2->active || loop2->doppler || loop2->sfx != loop->sfx)
            {
                continue;
            }
            loop2->mergeFrame = loopFrame;

            if (loop2->kill)
            {
                S_SpatializeOrigin( loop2->origin, 127, &left, &right);				// 3d
            }
            else
            {
                S_SpatializeOrigin( loop2->origin, 90,  &left, &right);				// sphere
            }

            loop2->sfx->lastTimeUsed = time;
            left_total += left;
            right_total += right;
        }
        if (left_total == 0 && right_total == 0)
        {
            continue;		// not audible
        }

        // allocate a channel
        ch = &loop_channels[numLoopChannels];

        if (left_total > 255)
        {
            left_total = 255;
        }
        if (right_total > 255)
        {
            right_total = 255;
        }

        ch->master_vol = 127;
        ch->leftvol = left_total;
        ch->rightvol = right_total;
        ch->thesfx = loop->sfx;
        ch->doppler = loop->doppler;
        ch->dopplerScale = loop->dopplerScale;
        ch->oldDopplerScale = loop->oldDopplerScale;
        numLoopChannels++;
        if (numLoopChannels == MAX_CHANNELS)
        {
            return;
        }
    }
}

//=============================================================================

/*
=================
S_ByteSwapRawSamples
 
If raw data has been loaded in little endien binary form, this must be done.
If raw data was calculated, as with ADPCM, this should not be called.
=================
*/
void S_ByteSwapRawSamples( int samples, int width, int s_channels, const byte *data )
{
    int		i;

    if ( width != 2 )
    {
        return;
    }
    if ( LittleShort( 256 ) == 256 )
    {
        return;
    }

    if ( s_channels == 2 )
    {
        samples <<= 1;
    }
    for ( i = 0 ; i < samples ; i++ )
    {
        ((short *)data)[i] = LittleShort( ((short *)data)[i] );
    }
}

portable_samplepair_t *S_GetRawSamplePointer()
{
    return s_rawsamples;
}

/*
============
S_RawSamples
 
Music streaming
============
*/
void S_RawSamples( int samples, int rate, int width, int s_channels, const byte *data, float volume )
{
    int		i;
    int		src, dst;
    float	scale;
    int		intVolume;

    if ( !s_soundStarted || s_soundMuted )
    {
        return;
    }

    intVolume = 256 * volume;

    if ( s_rawend < s_soundtime )
    {
        Com_DPrintf( "S_RawSamples: resetting minimum: %i < %i\n", s_rawend, s_soundtime );
        s_rawend = s_soundtime;
    }

    scale = (float)rate / dma.speed;

//Com_Printf ("%i < %i < %i\n", s_soundtime, s_paintedtime, s_rawend);
    if (s_channels == 2 && width == 2)
    {
        if (scale == 1.0)
        {	// optimized case
            for (i=0 ; i<samples ; i++)
            {
                dst = s_rawend&(MAX_RAW_SAMPLES-1);
                s_rawend++;
                s_rawsamples[dst].left = ((short *)data)[i*2] * intVolume;
                s_rawsamples[dst].right = ((short *)data)[i*2+1] * intVolume;
            }
        }
        else
        {
            for (i=0 ; ; i++)
            {
                src = i*scale;
                if (src >= samples)
                    break;
                dst = s_rawend&(MAX_RAW_SAMPLES-1);
                s_rawend++;
                s_rawsamples[dst].left = ((short *)data)[src*2] * intVolume;
                s_rawsamples[dst].right = ((short *)data)[src*2+1] * intVolume;
            }
        }
    }
    else if (s_channels == 1 && width == 2)
    {
        for (i=0 ; ; i++)
        {
            src = i*scale;
            if (src >= samples)
                break;
            dst = s_rawend&(MAX_RAW_SAMPLES-1);
            s_rawend++;
            s_rawsamples[dst].left = ((short *)data)[src] * intVolume;
            s_rawsamples[dst].right = ((short *)data)[src] * intVolume;
        }
    }
    else if (s_channels == 2 && width == 1)
    {
        intVolume *= 256;

        for (i=0 ; ; i++)
        {
            src = i*scale;
            if (src >= samples)
                break;
            dst = s_rawend&(MAX_RAW_SAMPLES-1);
            s_rawend++;
            s_rawsamples[dst].left = ((char *)data)[src*2] * intVolume;
            s_rawsamples[dst].right = ((char *)data)[src*2+1] * intVolume;
        }
    }
    else if (s_channels == 1 && width == 1)
    {
        intVolume *= 256;

        for (i=0 ; ; i++)
        {
            src = i*scale;
            if (src >= samples)
                break;
            dst = s_rawend&(MAX_RAW_SAMPLES-1);
            s_rawend++;
            s_rawsamples[dst].left = (((byte *)data)[src]-128) * intVolume;
            s_rawsamples[dst].right = (((byte *)data)[src]-128) * intVolume;
        }
    }

    if ( s_rawend > s_soundtime + MAX_RAW_SAMPLES )
    {
        Com_DPrintf( "S_RawSamples: overflowed %i > %i\n", s_rawend, s_soundtime );
    }
}

//=============================================================================

/*
=====================
S_UpdateEntityPosition
 
let the sound system know where an entity currently is
======================
*/
void S_UpdateEntityPosition( int entityNum, const vec3_t origin )
{
    if ( entityNum < 0 || entityNum > MAX_GENTITIES )
    {
        Com_Error( ERR_DROP, "S_UpdateEntityPosition: bad entitynum %i", entityNum );
    }
    VectorCopy( origin, loopSounds[entityNum].origin );
}


/*
============
S_Respatialize
 
Change the volumes of all the playing sounds for changes in their positions
============
*/
void S_Respatialize( unsigned int entityNum, const vec3_t head, vec3_t axis[3], int inwater )
{
    int			i;
    channel_t	*ch;
    vec3_t		origin;

    if ( !s_soundStarted || s_soundMuted )
    {
        return;
    }

    listener_number = entityNum;
    VectorCopy(head, listener_origin);
    VectorCopy(axis[0], listener_axis[0]);
    VectorCopy(axis[1], listener_axis[1]);
    VectorCopy(axis[2], listener_axis[2]);

    // update spatialization for dynamic sounds
    ch = s_channels;
    for ( i = 0 ; i < MAX_CHANNELS ; i++, ch++ )
    {
        if ( !ch->thesfx )
        {
            continue;
        }
        // anything coming from the view entity will always be full volume
        if (ch->entnum == listener_number)
        {
            ch->leftvol = ch->master_vol;
            ch->rightvol = ch->master_vol;
        }
        else
        {
            if (ch->fixed_origin)
            {
                VectorCopy( ch->origin, origin );
            }
            else
            {
                VectorCopy( loopSounds[ ch->entnum ].origin, origin );
            }

            S_SpatializeOrigin (origin, ch->master_vol, &ch->leftvol, &ch->rightvol);
        }
    }

    // add loopsounds
    S_AddLoopSounds ();
}


/*
========================
S_ScanChannelStarts
 
Returns qtrue if any new sounds were started since the last mix
========================
*/
qboolean S_ScanChannelStarts( void )
{
    channel_t		*ch;
    int				i;
    qboolean		newSamples;

    newSamples = qfalse;
    ch = s_channels;

    for (i=0; i<MAX_CHANNELS ; i++, ch++)
    {
        if ( !ch->thesfx )
        {
            continue;
        }
        // if this channel was just started this frame,
        // set the sample count to it begins mixing
        // into the very first sample
        if ( ch->startSample == START_SAMPLE_IMMEDIATE )
        {
            ch->startSample = s_paintedtime;
            newSamples = qtrue;
            continue;
        }

        // if it is completely finished by now, clear it
        if ( ch->startSample + (ch->thesfx->soundLength) <= s_paintedtime )
        {
            S_ChannelFree(ch);
        }
    }

    return newSamples;
}

/*
============
S_Update
 
Called once each time through the main loop
============
*/
void S_Update( void )
{
    int			i;
    int			total;
    channel_t	*ch;

    if ( !s_soundStarted || s_soundMuted )
    {
        Com_DPrintf ("not started or muted\n");
        return;
    }

    //
    // debugging output
    //
    if ( s_show->integer == 2 )
    {
        total = 0;
        ch = s_channels;
        for (i=0 ; i<MAX_CHANNELS; i++, ch++)
        {
            if (ch->thesfx && (ch->leftvol || ch->rightvol) )
            {
                Com_Printf ("%f %f %s\n", ch->leftvol, ch->rightvol, ch->thesfx->soundName);
                total++;
            }
        }

        Com_Printf ("----(%i)---- painted: %i\n", total, s_paintedtime);
    }

    // add raw data from streamed samples
    S_UpdateBackgroundTrack();

    // mix some sound
    S_Update_();
}

void S_GetSoundtime(void)
{
    int		samplepos;
    static	int		buffers;
    static	int		oldsamplepos;
    int		fullsamples;

    fullsamples = dma.samples / dma.channels;

    // it is possible to miscount buffers if it has wrapped twice between
    // calls to S_Update.  Oh well.
    samplepos = SNDDMA_GetDMAPos();
    if (samplepos < oldsamplepos)
    {
        buffers++;					// buffer wrapped

        if (s_paintedtime > 0x40000000)
        {	// time to chop things off to avoid 32 bit limits
            buffers = 0;
            s_paintedtime = fullsamples;
            S_StopAllSounds ();
        }
    }
    oldsamplepos = samplepos;

    s_soundtime = buffers*fullsamples + samplepos/dma.channels;

#if 0
// check to make sure that we haven't overshot
    if (s_paintedtime < s_soundtime)
    {
        Com_DPrintf ("S_Update_ : overflow\n");
        s_paintedtime = s_soundtime;
    }
#endif

    if ( dma.submission_chunk < 256 )
    {
        s_paintedtime = s_soundtime + s_mixPreStep->value * dma.speed;
    }
    else
    {
        s_paintedtime = s_soundtime + dma.submission_chunk;
    }
}


void S_Update_(void)
{
    unsigned        endtime;
    int				samps;
    static			float	lastTime = 0.0f;
    float			ma, op;
    float			thisTime, sane;
    static			int ot = -1;

    if ( !s_soundStarted || s_soundMuted )
    {
        return;
    }

    thisTime = Com_Milliseconds();

    // Updates s_soundtime
    S_GetSoundtime();

    if (s_soundtime == ot)
    {
        return;
    }
    ot = s_soundtime;

    // clear any sound effects that end before the current time,
    // and start any new sounds
    S_ScanChannelStarts();

    sane = thisTime - lastTime;
    if (sane<11)
    {
        sane = 11;			// 85hz
    }

    ma = s_mixahead->value * dma.speed;
    op = s_mixPreStep->value + sane*dma.speed*0.01;

    if (op < ma)
    {
        ma = op;
    }

    // mix ahead of current position
    endtime = s_soundtime + ma;

    // mix to an even submission block size
    endtime = (endtime + dma.submission_chunk-1)
              & ~(dma.submission_chunk-1);

    // never mix more than the complete buffer
    samps = dma.samples >> (dma.channels-1);
    if (endtime - s_soundtime > samps)
        endtime = s_soundtime + samps;



    SNDDMA_BeginPainting ();

    S_PaintChannels (endtime);

    SNDDMA_Submit ();

    lastTime = thisTime;
}

/*
===============================================================================
 
console functions
 
===============================================================================
*/

void S_Play_f( void )
{
    int 		i;
    sfxHandle_t	h;
    char		name[256];

    i = 1;
    while ( i<Cmd_Argc() )
    {
        if ( !Q_strrchr(Cmd_Argv(i), '.') )
        {
            Com_sprintf( name, sizeof(name), "%s.wav", Cmd_Argv(1) );
        }
        else
        {
            Q_strncpyz( name, Cmd_Argv(i), sizeof(name) );
        }
        h = S_RegisterSound( name, qfalse );
        if( h )
        {
            S_StartLocalSound( h, CHAN_LOCAL_SOUND );
        }
        i++;
    }
}

void S_Music_f( void )
{
    int		c;

    c = Cmd_Argc();

    if ( c == 2 )
    {
        S_StartBackgroundTrack( Cmd_Argv(1), Cmd_Argv(1) );
        s_backgroundLoop[0] = 0;
    }
    else if ( c == 3 )
    {
        S_StartBackgroundTrack( Cmd_Argv(1), Cmd_Argv(2) );
    }
    else
    {
        Com_Printf ("music <musicfile> [loopfile]\n");
        return;
    }

}

void S_SoundList_f( void )
{
    int		i;
    sfx_t	*sfx;
    int		size, total;
    char	type[4][16];
    char	mem[2][16];

    strcpy(type[0], "16bit");
    strcpy(type[1], "adpcm");
    strcpy(type[2], "daub4");
    strcpy(type[3], "mulaw");
    strcpy(mem[0], "paged out");
    strcpy(mem[1], "resident ");
    total = 0;
    for (sfx=s_knownSfx, i=0 ; i<s_numSfx ; i++, sfx++)
    {
        size = sfx->soundLength;
        total += size;
        Com_Printf("%6i[%s] : %s[%s]\n", size, type[sfx->soundCompressionMethod], sfx->soundName, mem[sfx->inMemory] );
    }
    Com_Printf ("Total resident: %i\n", total);
    S_DisplayFreeMemory();
}


/*
===============================================================================
 
background music functions
 
===============================================================================
*/

int	FGetLittleLong( fileHandle_t f )
{
    int		v;

    FS_Read( &v, sizeof(v), f );

    return LittleLong( v);
}

int	FGetLittleShort( fileHandle_t f )
{
    short	v;

    FS_Read( &v, sizeof(v), f );

    return LittleShort( v);
}

// returns the length of the data in the chunk, or 0 if not found
int S_FindWavChunk( fileHandle_t f, char *chunk )
{
    char	name[5];
    int		len;
    int		r;

    name[4] = 0;
    len = 0;
    r = FS_Read( name, 4, f );
    if ( r != 4 )
    {
        return 0;
    }
    len = FGetLittleLong( f );
    if ( len < 0 || len > 0xfffffff )
    {
        len = 0;
        return 0;
    }
    len = (len + 1 ) & ~1;		// pad to word boundary
//	s_nextWavChunk += len + 8;

    if ( strcmp( name, chunk ) )
    {
        return 0;
    }

    return len;
}

/*
======================
S_StopBackgroundTrack
======================
*/
void S_StopBackgroundTrack( void )
{
    if ( !s_backgroundFile )
    {
        return;
    }
    Sys_EndStreamedFile( s_backgroundFile );
    FS_FCloseFile( s_backgroundFile );
    s_backgroundFile = 0;
    s_rawend = 0;
}

/*
======================
S_StartBackgroundTrack
======================
*/
void S_StartBackgroundTrack( const char *intro, const char *loop )
{
    int		len;
    char	dump[16];
    char	name[MAX_QPATH];

    if ( !intro )
    {
        intro = "";
    }
    if ( !loop || !loop[0] )
    {
        loop = intro;
    }
    Com_DPrintf( "S_StartBackgroundTrack( %s, %s )\n", intro, loop );

    Q_strncpyz( name, intro, sizeof( name ) - 4 );
    COM_DefaultExtension( name, sizeof( name ), ".wav" );

    if ( !intro[0] )
    {
        return;
    }

    Q_strncpyz( s_backgroundLoop, loop, sizeof( s_backgroundLoop ) );

    // close the background track, but DON'T reset s_rawend
    // if restarting the same back ground track
    if ( s_backgroundFile )
    {
        Sys_EndStreamedFile( s_backgroundFile );
        FS_FCloseFile( s_backgroundFile );
        s_backgroundFile = 0;
    }

    //
    // open up a wav file and get all the info
    //
    FS_FOpenFileRead( name, &s_backgroundFile, qtrue );
    if ( !s_backgroundFile )
    {
        Com_Printf( S_COLOR_YELLOW "WARNING: couldn't open music file %s\n", name );
        return;
    }

    // skip the riff wav header

    FS_Read(dump, 12, s_backgroundFile);

    if ( !S_FindWavChunk( s_backgroundFile, "fmt " ) )
    {
        Com_Printf( "No fmt chunk in %s\n", name );
        FS_FCloseFile( s_backgroundFile );
        s_backgroundFile = 0;
        return;
    }

    // save name for soundinfo
    s_backgroundInfo.format = FGetLittleShort( s_backgroundFile );
    s_backgroundInfo.channels = FGetLittleShort( s_backgroundFile );
    s_backgroundInfo.rate = FGetLittleLong( s_backgroundFile );
    FGetLittleLong(  s_backgroundFile );
    FGetLittleShort(  s_backgroundFile );
    s_backgroundInfo.width = FGetLittleShort( s_backgroundFile ) / 8;

    if ( s_backgroundInfo.format != WAV_FORMAT_PCM )
    {
        FS_FCloseFile( s_backgroundFile );
        s_backgroundFile = 0;
        Com_Printf("Not a microsoft PCM format wav: %s\n", name);
        return;
    }

    if ( s_backgroundInfo.channels != 2 || s_backgroundInfo.rate != 22050 )
    {
        Com_Printf(S_COLOR_YELLOW "WARNING: music file %s is not 22k stereo\n", name );
    }

    if ( ( len = S_FindWavChunk( s_backgroundFile, "data" ) ) == 0 )
    {
        FS_FCloseFile( s_backgroundFile );
        s_backgroundFile = 0;
        Com_Printf("No data chunk in %s\n", name);
        return;
    }

    s_backgroundInfo.samples = len / (s_backgroundInfo.width * s_backgroundInfo.channels);

    s_backgroundSamples = s_backgroundInfo.samples;

    //
    // start the background streaming
    //
    Sys_BeginStreamedFile( s_backgroundFile, 0x10000 );
}

/*
======================
S_UpdateBackgroundTrack
======================
*/
void S_UpdateBackgroundTrack( void )
{
    int		bufferSamples;
    int		fileSamples;
    byte	raw[30000];		// just enough to fit in a mac stack frame
    int		fileBytes;
    int		r;
    static	float	musicVolume = 0.5f;

    if ( !s_backgroundFile )
    {
        return;
    }

    // graeme see if this is OK
    musicVolume = (musicVolume + (s_musicVolume->value * 2))/4.0f;

    // don't bother playing anything if musicvolume is 0
    if ( musicVolume <= 0 )
    {
        return;
    }

    // see how many samples should be copied into the raw buffer
    if ( s_rawend < s_soundtime )
    {
        s_rawend = s_soundtime;
    }

    while ( s_rawend < s_soundtime + MAX_RAW_SAMPLES )
    {
        bufferSamples = MAX_RAW_SAMPLES - (s_rawend - s_soundtime);

        // decide how much data needs to be read from the file
        fileSamples = bufferSamples * s_backgroundInfo.rate / dma.speed;

        // don't try and read past the end of the file
        if ( fileSamples > s_backgroundSamples )
        {
            fileSamples = s_backgroundSamples;
        }

        // our max buffer size
        fileBytes = fileSamples * (s_backgroundInfo.width * s_backgroundInfo.channels);
        if ( fileBytes > sizeof(raw) )
        {
            fileBytes = sizeof(raw);
            fileSamples = fileBytes / (s_backgroundInfo.width * s_backgroundInfo.channels);
        }

        r = Sys_StreamedRead( raw, 1, fileBytes, s_backgroundFile );
        if ( r != fileBytes )
        {
            Com_Printf("StreamedRead failure on music track\n");
            S_StopBackgroundTrack();
            return;
        }

        // byte swap if needed
        S_ByteSwapRawSamples( fileSamples, s_backgroundInfo.width, s_backgroundInfo.channels, raw );

        // add to raw buffer
        S_RawSamples( fileSamples, s_backgroundInfo.rate,
                      s_backgroundInfo.width, s_backgroundInfo.channels, raw, musicVolume );

        s_backgroundSamples -= fileSamples;
        if ( !s_backgroundSamples )
        {
            // loop
            if (s_backgroundLoop[0])
            {
                Sys_EndStreamedFile( s_backgroundFile );
                FS_FCloseFile( s_backgroundFile );
                s_backgroundFile = 0;
                S_StartBackgroundTrack( s_backgroundLoop, s_backgroundLoop );
                if ( !s_backgroundFile )
                {
                    return;		// loop failed to restart
                }
            }
            else
            {
                s_backgroundFile = 0;
                return;
            }
        }
    }
}


/*
======================
S_FreeOldestSound
======================
*/

void S_FreeOldestSound()
{
    int	i, oldest, used;
    sfx_t	*sfx;
    sndBuffer	*buffer, *nbuffer;

    oldest = Com_Milliseconds();
    used = 0;

    for (i=1 ; i < s_numSfx ; i++)
    {
        sfx = &s_knownSfx[i];
        if (sfx->inMemory && sfx->lastTimeUsed<oldest)
        {
            used = i;
            oldest = sfx->lastTimeUsed;
        }
    }

    sfx = &s_knownSfx[used];

    Com_DPrintf("S_FreeOldestSound: freeing sound %s\n", sfx->soundName);

    buffer = sfx->soundData;
    while(buffer != NULL)
    {
        nbuffer = buffer->next;
        SND_free(buffer);
        buffer = nbuffer;
    }
    sfx->inMemory = qfalse;
    sfx->soundData = NULL;
}
